﻿namespace Sai.Cecil
{
	using System;
	using Sai.Core.Introspection;
	using Mono.Cecil;
	
	/// <summary>
	/// Factory type extension to build a declarations from equivalent mono definition objects.
	/// </summary>
	public static class TypeDefinitionExtensions
	{
		/// <summary>
		/// Adds public typedefinitions to an assembly introspection object.
		/// </summary>
		/// <param name="discoveredType">The type definition.</param>
		/// <param name="introspection">The introspected assembly.</param>
		public static void AddTo(this TypeDefinition discoveredType, IntrospectedAssembly introspection)
		{
			if (discoveredType.IsPublic)
			{
				introspection.Add(discoveredType.BuildTypeDeclaration());
			}
		}

		/// <summary>
		/// Creates an public type object (class, interface, enum) from the equivalent mono type definition.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <returns>A type declaration.</returns>
		public static ITypeDeclaration BuildTypeDeclaration(this TypeDefinition discovered)
		{
			ITypeDeclaration publicType = null;

			// check enum first - cecil considers 
			// both classes and enums are classes
			if (discovered.IsEnum)
			{
				publicType = discovered.BuildEnum();
			}
			else if (discovered.IsClass)
			{
				publicType = discovered.BuildClass();
			}
			else if (discovered.IsInterface)
			{
				publicType = discovered.BuildInterface();
			}

			return publicType;
		}

		/// <summary>
		/// Create an enum declaration from a type definition.
		/// </summary>
		/// <param name="discovered">The discovered type.</param>
		/// <returns>The enum object.</returns>
		private static EnumTypeDeclaration BuildEnum(this TypeDefinition discovered)
		{
			if (discovered.IsEnum)
			{
				var enumType = new EnumTypeDeclaration(
												discovered.ToTypeName(),
												discovered.BuildObsoleteMarker());

				discovered.PopulateAllowedValues(enumType);

				return enumType;
			}

			throw new ArgumentException("Type definition is not an enum", "discovered");
		}

		/// <summary>
		/// Creates an interface object from a type definition.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <returns>The interace object.</returns>
		private static InterfaceDeclaration BuildInterface(this TypeDefinition discovered)
		{
			var interfaceType = new InterfaceDeclaration(
											discovered.ToTypeName(),
											discovered.BuildObsoleteMarker());

			discovered.PopulateEvents(interfaceType);
			discovered.PopulateProperties(interfaceType);
			discovered.PopulateMethods(interfaceType);
			
			if (discovered.HasInterfaces)
			{
				foreach (var inter in discovered.Interfaces)
				{
                    if (inter.IsPublicInterface())
                    {
                        interfaceType.Implementing(inter.ToTypeName());
                    }
				}
			}

			return interfaceType;
		}

		/// <summary>
		/// Is this type derived from something other than system.object?.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <returns>True if derived from anything but system.object.</returns>
		private static bool HasSuperClass(this TypeDefinition discovered)
		{
			const string BaseObjectName = "System.Object";

			return discovered.BaseType != null && discovered.BaseType.FullName != BaseObjectName;
		}

		/// <summary>
		/// Creates a class object from a type definition.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <returns>The class object.</returns>
		private static ClassDeclaration BuildClass(this TypeDefinition discovered)
		{
			var classType = new ClassDeclaration(
											discovered.ToTypeName(),
											discovered.BuildObsoleteMarker())
											{
												IsAbstract = discovered.IsAbstract,
												IsSealed = discovered.IsSealed
											};

			discovered.PopulateEvents(classType);
			discovered.PopulateFields(classType);
			discovered.PopulateProperties(classType);
			discovered.PopulateMethods(classType);

			if (discovered.HasSuperClass())
			{
				classType.DerivedFrom(discovered.BaseType.ToTypeName());
			}

			if (discovered.HasInterfaces)
			{
				foreach (var inter in discovered.Interfaces)
				{
                    if (inter.IsPublicInterface())
                    {
        				classType.Implementing(inter.ToTypeName());
                    }
				}
			}

			return classType;
		}

		/// <summary>
		/// Populates a class/interface definition with event objects found in the type definition.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The class or interface object.</param>
		private static void PopulateEvents(this TypeDefinition discovered, ReferenceTypeDeclaration publicType)
		{
			if (discovered.HasEvents)
			{
				foreach (var e in discovered.Events)
				{
					publicType.Add(e.ToEventDeclaration());
				}
			}
		}

		/// <summary>
		/// Populate class with fields found in a type.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The class declaration.</param>
		private static void PopulateFields(this TypeDefinition discovered, ClassDeclaration publicType)
		{
			if (discovered.HasFields)
			{
				foreach (var field in discovered.Fields)
				{
					if (field.IsIncluded())
					{
						publicType.Add(field.ToFieldDeclaration());
					}
				}
			}
		}

		/// <summary>
		/// Creates allowed values in enum from type.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The enum declaration.</param>
		private static void PopulateAllowedValues(this TypeDefinition discovered, EnumTypeDeclaration publicType)
		{
			if (discovered.HasFields)
			{
				foreach (var field in discovered.Fields)
				{
					if (field.IsPublicField())
					{
						long value = 0;

						try
						{
							value = Convert.ToInt64(field.Constant);
						}
						catch (OverflowException)
						{
							value = 0;
						}
						catch (InvalidCastException)
						{
							value = 0;
						}

						publicType.Add(field.Name, value);
					}
				}
			}
		}

		/// <summary>
		/// Populates class/interface with properties from type.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The ref type declaration.</param>
		private static void PopulateProperties(this TypeDefinition discovered, ReferenceTypeDeclaration publicType)
		{
			if (discovered.HasProperties)
			{
				foreach (var property in discovered.Properties)
				{
					var declaration = property.ToPropertyDeclaration();

					// only add if something is public
					if (declaration.HasPublicGetter || declaration.HasPublicSetter)
					{
						publicType.Add(declaration);
					}
				}
			}
		}

		/// <summary>
		/// Populates class with methods from type.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The class declaration.</param>
		private static void PopulateMethods(this TypeDefinition discovered, ClassDeclaration publicType)
		{
			if (discovered.HasMethods)
			{
				foreach (var method in discovered.Methods)
				{
                    if (method.IsSpecialName)
                    {
                        // constructors etc.
                        if (method.IsConstructor)
                        {
                            if (method.IsStatic)
                            {
                                publicType.Add(method.ToStaticConstructorDeclaration(discovered));
                            }
                            else if (method.IsPublic)
                            {
                                publicType.Add(method.ToConstructorDeclaration(discovered));
                            }
                        }
                    }
                    else
                    {
                        if (method.IsFinalizer())
                        {
                            publicType.Add(method.ToFinalizerDeclaration(discovered));
                        }
                        else if (method.IsIncluded())
                        {
                            publicType.Add(method.ToMethodDeclaration());
                        }
                    }
				}
			}
		}

		/// <summary>
		/// Populate interface with methods from a type.
		/// </summary>
		/// <param name="discovered">The type definition.</param>
		/// <param name="publicType">The interface declaration.</param>
		private static void PopulateMethods(this TypeDefinition discovered, InterfaceDeclaration publicType)
		{
			if (discovered.HasMethods)
			{
				foreach (var method in discovered.Methods)
				{
					// do we need not private instead of public to cover inherited
					// non-internal cases ???
					if (method.IsInterfaceMethod())
					{
						publicType.Add(method.ToMethodDeclaration());
					}
				}
			}
		}

        private static bool IsPublicInterface(this TypeReference candidateInterace)
        {
            TypeDefinition interfaceDefinition = candidateInterace as TypeDefinition;

            if (interfaceDefinition != null)
            {
                return interfaceDefinition.IsPublic;
            }

            return false;
        }
	}
}
